package azblob_test

import (
	"context"
	"crypto/rand"
	"fmt"
	"io"
	"net"
	"net/http"

	chk "gopkg.in/check.v1"
	"github.com/Azure/azure-storage-blob-go/2017-07-29/azblob"
)

// Testings for RetryReader
// This reader return one byte through each Read call
type perByteReader struct {
	RandomBytes []byte // Random generated bytes

	byteCount              int // Bytes can be returned before EOF
	currentByteIndex       int // Bytes that have already been returned.
	doInjectError          bool
	doInjectErrorByteIndex int
	doInjectTimes          int
	injectedError          error
}

func newPerByteReader(byteCount int) *perByteReader {
	perByteReader := perByteReader{
		byteCount: byteCount,
	}

	perByteReader.RandomBytes = make([]byte, byteCount)
	rand.Read(perByteReader.RandomBytes)

	return &perByteReader
}

func (r *perByteReader) Read(b []byte) (n int, err error) {
	if r.doInjectError && r.doInjectErrorByteIndex == r.currentByteIndex && r.doInjectTimes > 0 {
		r.doInjectTimes--
		return 0, r.injectedError
	}

	if r.currentByteIndex < r.byteCount {
		n = copy(b, r.RandomBytes[r.currentByteIndex:r.currentByteIndex+1])
		r.currentByteIndex += n
		return
	}

	return 0, io.EOF
}

func (r *perByteReader) Close() error {
	return nil
}

// Test normal retry succeed, note initial response not provided.
func (r *aztestsSuite) TestRetryReaderReadWithRetry(c *chk.C) {
	byteCount := 1
	body := newPerByteReader(byteCount)
	body.doInjectError = true
	body.doInjectErrorByteIndex = 0
	body.doInjectTimes = 1
	body.injectedError = &net.DNSError{IsTemporary: true}

	getter := func(ctx context.Context, info azblob.HTTPGetterInfo) (*http.Response, error) {
		r := http.Response{}
		body.currentByteIndex = int(info.Offset)
		r.Body = body

		return &r, nil
	}

	httpGetterInfo := azblob.HTTPGetterInfo{Offset: 0, Count: int64(byteCount)}
	initResponse, err := getter(context.Background(), httpGetterInfo)
	c.Assert(err, chk.IsNil)

	retryReader := azblob.NewRetryReader(context.Background(), initResponse, httpGetterInfo, azblob.RetryReaderOptions{MaxRetryRequests: 1}, getter)

	// should fail and succeed through retry
	can := make([]byte, 1)
	n, err := retryReader.Read(can)
	c.Assert(n, chk.Equals, 1)
	c.Assert(err, chk.IsNil)

	// should return EOF
	n, err = retryReader.Read(can)
	c.Assert(n, chk.Equals, 0)
	c.Assert(err, chk.Equals, io.EOF)
}

// Test normal retry fail as retry Count not enough.
func (r *aztestsSuite) TestRetryReaderReadNegativeNormalFail(c *chk.C) {
	byteCount := 1
	body := newPerByteReader(byteCount)
	body.doInjectError = true
	body.doInjectErrorByteIndex = 0
	body.doInjectTimes = 2
	body.injectedError = &net.DNSError{IsTemporary: true}

	startResponse := http.Response{}
	startResponse.Body = body

	getter := func(ctx context.Context, info azblob.HTTPGetterInfo) (*http.Response, error) {
		r := http.Response{}
		body.currentByteIndex = int(info.Offset)
		r.Body = body

		return &r, nil
	}

	retryReader := azblob.NewRetryReader(context.Background(), &startResponse, azblob.HTTPGetterInfo{Offset: 0, Count: int64(byteCount)}, azblob.RetryReaderOptions{MaxRetryRequests: 1}, getter)

	// should fail
	can := make([]byte, 1)
	n, err := retryReader.Read(can)
	c.Assert(n, chk.Equals, 0)
	c.Assert(err, chk.Equals, body.injectedError)
}

// Test boundary case when Count equals to 0 and fail.
func (r *aztestsSuite) TestRetryReaderReadCount0(c *chk.C) {
	byteCount := 1
	body := newPerByteReader(byteCount)
	body.doInjectError = true
	body.doInjectErrorByteIndex = 1
	body.doInjectTimes = 1
	body.injectedError = &net.DNSError{IsTemporary: true}

	startResponse := http.Response{}
	startResponse.Body = body

	getter := func(ctx context.Context, info azblob.HTTPGetterInfo) (*http.Response, error) {
		r := http.Response{}
		body.currentByteIndex = int(info.Offset)
		r.Body = body

		return &r, nil
	}

	retryReader := azblob.NewRetryReader(context.Background(), &startResponse, azblob.HTTPGetterInfo{Offset: 0, Count: int64(byteCount)}, azblob.RetryReaderOptions{MaxRetryRequests: 1}, getter)

	// should consume the only byte
	can := make([]byte, 1)
	n, err := retryReader.Read(can)
	c.Assert(n, chk.Equals, 1)
	c.Assert(err, chk.IsNil)

	// should not read when Count=0, and should return EOF
	n, err = retryReader.Read(can)
	c.Assert(n, chk.Equals, 0)
	c.Assert(err, chk.Equals, io.EOF)
}

func (r *aztestsSuite) TestRetryReaderReadNegativeNonRetriableError(c *chk.C) {
	byteCount := 1
	body := newPerByteReader(byteCount)
	body.doInjectError = true
	body.doInjectErrorByteIndex = 0
	body.doInjectTimes = 1
	body.injectedError = fmt.Errorf("not retriable error")

	startResponse := http.Response{}
	startResponse.Body = body

	getter := func(ctx context.Context, info azblob.HTTPGetterInfo) (*http.Response, error) {
		r := http.Response{}
		body.currentByteIndex = int(info.Offset)
		r.Body = body

		return &r, nil
	}

	retryReader := azblob.NewRetryReader(context.Background(), &startResponse, azblob.HTTPGetterInfo{Offset: 0, Count: int64(byteCount)}, azblob.RetryReaderOptions{MaxRetryRequests: 2}, getter)

	dest := make([]byte, 1)
	_, err := retryReader.Read(dest)
	c.Assert(err, chk.Equals, body.injectedError)
}

func (r *aztestsSuite) TestRetryReaderNewRetryReaderDefaultNegativePanic(c *chk.C) {
	startResponse := http.Response{}

	// Check getter
	c.Assert(func() { _ = azblob.NewRetryReader(context.Background(), &startResponse, azblob.HTTPGetterInfo{}, azblob.RetryReaderOptions{}, nil) }, chk.Panics, "getter must not be nil")

	getter := func(ctx context.Context, info azblob.HTTPGetterInfo) (*http.Response, error) { return nil, nil }
	// Check info.Count
	c.Assert(func() {
		_ = azblob.NewRetryReader(context.Background(), &startResponse, azblob.HTTPGetterInfo{Count: -1}, azblob.RetryReaderOptions{}, getter)
	}, chk.Panics, "info.Count must be >= 0")

	// Check o.MaxRetryRequests
	c.Assert(func() {
		_ = azblob.NewRetryReader(context.Background(), &startResponse, azblob.HTTPGetterInfo{}, azblob.RetryReaderOptions{MaxRetryRequests: -1}, getter)
	}, chk.Panics, "o.MaxRetryRequests must be >= 0")

}

// End testings for RetryReader
